#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <unistd.h>

#ifdef USE_CAPSICUM
#include <sys/capsicum.h>
#endif

#include "au.h"
#include "adev.h"
#include "common.h"

#ifndef AUOSS_DSP
#define AUOSS_DSP "/dev/dsp"
#endif

enum {
	A_NULL,
	A_ENC,
	A_RATE,
	A_CHAN
};

struct adev {
	uint32_t enc;
	int reset;
	int mode;
	int dsp;
};

static struct adev *ad_fdopen(int, int);

const struct a_lookup stab[] = {
	{ "enc", A_ENC },
	{ "encoding", A_ENC },
	{ "rate", A_RATE },
	{ "chan", A_CHAN },
	{ "channel", A_CHAN },
	{ "channels", A_CHAN },
	{ NULL, A_NULL }
};

struct adev *
ad_open(const char *dev, int mode)
{
	char path[PATH_MAX];
	struct adev *a;
	int fmode = (mode & A_WRITE) ? O_WRONLY : O_RDONLY;
	int fd;

	if (dev == NULL)
		dev = AUOSS_DSP;
	if (snprintf(path, sizeof(path), "%s", dev)
	    > sizeof(path))
		return (NULL);
	if ((fd = open(path, fmode)) >= 0)
		goto end;
	if (snprintf(path, sizeof(path), "/dev/%s", dev)
	    > sizeof(path))
		return (NULL);
	if ((fd = open(path, fmode)) >= 0)
		goto end;
	if (snprintf(path, sizeof(path), "/dev/dsp%s", dev)
	    > sizeof(path))
		return (NULL);
	if ((fd = open(path, fmode)) >= 0)
		goto end;
	return (NULL);
end:
	if ((a = ad_fdopen(fd, mode)) == NULL)
		close(fd);
	return (a);
}

struct adev *
ad_fpopen(FILE *fp, int mode)
{

	if (fp == NULL)
		ERR(EINVAL, NULL);
	return (ad_fdopen(fileno(fp), mode));
}

struct adev *
ad_fdopen(int fd, int mode)
{
	struct adev *a;
	int i = 1;

	if (fd < 0)
		ERR(EINVAL, NULL);
	if ((a = malloc(sizeof(struct adev))) == NULL)
		return (NULL);
#ifdef SNDCTL_DSP_COOKEDMODE
	/* Cooked mode does format conversion */
	if (ioctl(fd, SNDCTL_DSP_COOKEDMODE, &i) == -1) {
		free(a);
		return (NULL);
	}
#endif
	a->enc = 0;
	a->reset = 0;
	a->mode = mode;
	a->dsp = fd;
	return (a);
}

void
ad_close(struct adev *a)
{

	if (a == NULL)
		return;
	if (a->dsp >= 0)
		close(a->dsp);
	free(a);
}

int
ad_limit(struct adev *a)
{
#ifdef USE_CAPSICUM
	/* Also see AIOGFMT/AIOSFMT */
	const unsigned long ioctls[] = {
		SNDCTL_DSP_SETFMT,
		SNDCTL_DSP_SPEED,
		SNDCTL_DSP_CHANNELS,
		SNDCTL_DSP_SYNC,
		SNDCTL_DSP_RESET
	};
	cap_rights_t rights;

	if (a == NULL || a->dsp < 0)
		ERR(EINVAL, -1);
	(void)cap_rights_init(&rights, (a->mode & A_WRITE) ? CAP_WRITE
	    : CAP_READ, CAP_IOCTL);
	if (cap_rights_limit(a->dsp, &rights) == -1)
		return (-1);
	if (cap_ioctls_limit(a->dsp, ioctls, sizeof(ioctls) /
	    sizeof(ioctls[0])) == -1)
		return (-1);
	return (0);
#else

	ERR(ENOSYS, -1);
#endif
}

int
ad_get(struct adev *a, const char *s, ...)
{
	va_list ap;
	ssize_t r;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_ENC:
			switch (a->enc) {
			case AU_ULAW:
				/* FALLTHOURGH */
			case AU_PCM8:
				/* FALLTHOURGH */
			case AU_PCM16:
				/* FALLTHOURGH */
			case AU_PCM24:
				/* FALLTHOURGH */
			case AU_PCM32:
				*va_arg(ap, uint32_t *) = a->enc;
				break;
			case AU_FLOAT32:
				*va_arg(ap, uint32_t *) = AU_PCM24;
				break;
			case AU_FLOAT64:
				*va_arg(ap, uint32_t *) = AU_PCM32;
				break;
			case AU_ALAW:
				*va_arg(ap, uint32_t *) = a->enc;
				break;
			default:
				*va_arg(ap, uint32_t *) = AU_PCM16;
				break;
			}
			break;
		case A_RATE:
			goto err;
		case A_CHAN:
			goto err;
		default:
			goto err;
		}
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

int
ad_set(struct adev *a, const char *s, ...)
{
	va_list ap;
	ssize_t r;
	int n;
	int o;
	int t;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	if (a->reset) {
		if (ioctl(a->dsp, SNDCTL_DSP_SYNC) != 0 ||
		    ioctl(a->dsp, SNDCTL_DSP_RESET) != 0)
			return (-1);
		a->reset = 0;
	}
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_ENC:
			a->enc = va_arg(ap, uint32_t);
			switch (a->enc) {
			case AU_ULAW:
				o = AFMT_MU_LAW;
				break;
			case AU_PCM8:
				o = AFMT_S8;
				break;
			case AU_PCM16:
				o = AFMT_S16_BE;
				break;
			case AU_PCM24:
				o = AFMT_S24_BE;
				break;
			case AU_PCM32:
				o = AFMT_S32_BE;
				break;
			case AU_ALAW:
				o = AFMT_A_LAW;
				break;
			default:
				goto err;
			}
			t = o;
			if (ioctl(a->dsp, SNDCTL_DSP_SETFMT, &t) != 0 ||
			    t != o)
				goto err;
			break;
		case A_RATE:
			o = va_arg(ap, uint32_t);
			t = o;
			if (ioctl(a->dsp, SNDCTL_DSP_SPEED, &t) != 0 ||
			    t != o)
				goto err;
			break;
		case A_CHAN:
			o = va_arg(ap, uint32_t);
			t = o;
			if (ioctl(a->dsp, SNDCTL_DSP_CHANNELS, &t) != 0
			    || t != o)
				goto err;
			break;
		default:
			goto err;
		}
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

ssize_t
ad_read(struct adev *a, void *b, size_t s)
{

	if (a == NULL || a->dsp < 0 || b == NULL || (a->mode & A_WRITE))
		ERR(EINVAL, -1);
	if (s == 0)
		return (0);
	a->reset = 1;
	return (read(a->dsp, b, s));
}

ssize_t
ad_write(struct adev *a, const void *b, size_t s)
{

	if (a == NULL || a->dsp < 0 || b == NULL ||
	    !(a->mode & A_WRITE))
		ERR(EINVAL, -1);
	if (s == 0)
		return (0);
	a->reset = 1;
	return (write(a->dsp, b, s));
}
